package com.google.code.sms.service;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.List;

import com.google.code.sms.Logger;
import com.google.code.sms.dao.MediaElementDao;
import com.google.code.sms.dao.PlaylistDao;
import com.google.code.sms.domain.MediaElement;
import com.google.code.sms.domain.MediaFolder;
import com.google.code.sms.domain.Playlist;
import com.google.code.sms.util.Pair;
import com.google.code.sms.util.StringUtil;

/**
 * Provides services for loading and saving play-lists.
 *
 * @author Scott Ware
 */
public class PlaylistService {

    private static final Logger LOG = Logger.getLogger("PlaylistService");
    private MediaElementService mediaElementService;
    private MediaElementDao mediaElementDao;
    private PlaylistDao playlistDao;
    private SecurityService securityService;
    private SettingsService settingsService;

    public List<Playlist> getReadablePlaylistsForUser(String username) {
        return playlistDao.getReadablePlaylistsForUser(username);
    }

    public List<Playlist> getWritablePlaylistsForUser(String username) {

        // Admin users are allowed to modify all playlists that are visible to them.
        if (securityService.isAdmin(username)) {
            return getReadablePlaylistsForUser(username);
        }

        return playlistDao.getWritablePlaylistsForUser(username);
    }

    public Playlist getPlaylist(int id) {
        return playlistDao.getPlaylist(id);
    }

    public List<String> getPlaylistUsers(int playlistId) {
        return playlistDao.getPlaylistUsers(playlistId);
    }

    public List<MediaElement> getElementsInPlaylist(int id) {
        return mediaElementDao.getElementsInPlaylist(id);
    }

    public void setElementsInPlaylist(int id, List<MediaElement> elements) {
        playlistDao.setElementsInPlaylist(id, elements);
    }

    public void createPlaylist(Playlist playlist) {
        playlistDao.createPlaylist(playlist);
    }

    public void addPlaylistUser(int playlistId, String username) {
        playlistDao.addPlaylistUser(playlistId, username);
    }

    public void deletePlaylistUser(int playlistId, String username) {
        playlistDao.deletePlaylistUser(playlistId, username);
    }

    public boolean isReadAllowed(Playlist playlist, String username) {
        if (username == null) {
            return false;
        }
        if (username.equals(playlist.getUsername()) || playlist.isPublic()) {
            return true;
        }
        return playlistDao.getPlaylistUsers(playlist.getId()).contains(username);
    }

    public boolean isWriteAllowed(Playlist playlist, String username) {
        return username != null && username.equals(playlist.getUsername());
    }

    public void deletePlaylist(int id) {
        playlistDao.deletePlaylist(id);
    }

    public void updatePlaylist(Playlist playlist) {
        playlistDao.updatePlaylist(playlist);
    }

    private Pair<List<MediaElement>, List<String>> parseElements(byte[] playlist, PlaylistFormat playlistFormat) throws IOException {
        Pair<List<MediaElement>, List<String>> result = null;

        // Try with multiple encodings; use the one that finds the most elements.
        String[] encodings = {StringUtil.ENCODING_LATIN, StringUtil.ENCODING_UTF8, Charset.defaultCharset().name()};
        for (String encoding : encodings) {
            Pair<List<MediaElement>, List<String>> elements = parseElementsWithEncoding(playlist, playlistFormat, encoding);
            if (result == null || result.getFirst().size() < elements.getFirst().size()) {
                result = elements;
            }
        }
        return result;
    }

    private Pair<List<MediaElement>, List<String>> parseElementsWithEncoding(byte[] playlist, PlaylistFormat playlistFormat, String encoding) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(playlist), encoding));
        return playlistFormat.parse(reader, mediaElementService);
    }

    public void setPlaylistDao(PlaylistDao playlistDao) {
        this.playlistDao = playlistDao;
    }

    public void setMediaElementDao(MediaElementDao mediaElementDao) {
        this.mediaElementDao = mediaElementDao;
    }

    public void setMediaElementService(MediaElementService mediaElementService) {
        this.mediaElementService = mediaElementService;
    }

    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public void setSettingsService(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    /**
     * Abstract superclass for play-list formats.
     */
    private abstract class PlaylistFormat {

        public abstract Pair<List<MediaElement>, List<String>> parse(BufferedReader reader, MediaElementService mediaElementService) throws IOException;

        public abstract void format(List<MediaElement> elements, PrintWriter writer) throws IOException;

        protected MediaElement getMediaElement(String path) {
            try {
                File file = new File(path);
                if (!file.exists()) {
                    return null;
                }

                file = normalisePath(file);
                if (file == null) {
                    return null;
                }
                MediaElement mediaElement = mediaElementService.getMediaElement(file);
                if (mediaElement != null && mediaElement.exists()) {
                    return mediaElement;
                }
            } catch (SecurityException x) {
                // Ignored
            } catch (IOException x) {
                // Ignored
            }
            return null;
        }

        /**
         * Paths in an external play-list may not have the same upper/lower case
         * as in the (case sensitive) media_file table. This methods attempts to
         * normalise the external path to match the one stored in the table.
         */
        private File normalisePath(File file) throws IOException {

            // Find the most specific media folder.
            String canonicalPath = file.getCanonicalPath();
            MediaFolder containingMediaFolder = null;
            for (MediaFolder mediaFolder : settingsService.getAllMediaFolders()) {
                String mediaFolderPath = mediaFolder.getPath();
                if (canonicalPath.toLowerCase().startsWith(mediaFolderPath.toLowerCase())) {
                    if (containingMediaFolder == null || containingMediaFolder.getPath().length() < mediaFolderPath.length()) {
                        containingMediaFolder = mediaFolder;
                    }
                }
            }

            if (containingMediaFolder == null) {
                return null;
            }

            return new File(containingMediaFolder.getPath() + canonicalPath.substring(containingMediaFolder.getPath().length()));
            // TODO: Consider slashes.
        }
    }
}
